﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Spanner
{
    public partial class UI
    {
        /// <summary>
        /// JSExpr<![CDATA[<T>]]>(fmt, arg1, arg2, ..., argn) is equivalent to the JavaScript expression represented by fmt
        /// with the first # character in fmt replaced with the representation of arg1, the second # with the
        /// representation of arg2, ..., the nth @ with the represenation of argn.  The resulting expression is given the
        /// type T.
        /// 
        /// Example: JSExpr<![CDATA[<element>]]>("document") is equivalent to the JavaScript, document.
        /// Example: JSExpr<![CDATA[<int>]]>("#.length", xs) is equivalent to the JavaScript, xs.length.
        /// Example: JSExpr<![CDATA[<int>]]>("#.indexOf(#)", xs, x) is equivalent to the JavaScript, xs.indexOf(x).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="fmt"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        public static Expr<T> JSExpr<T>(string fmt, params object[] args)
        {
            return new JSExpr<T>(fmt, args.Cast<ITerm>().ToArray());
        }

        /// <summary>
        /// Cond(cond, valueIfT, valueIfF) is equivalent to the JavaScript (cond ? valueIfT : valueIfF).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="cond"></param>
        /// <param name="valueIfT"></param>
        /// <param name="valueIfF"></param>
        /// <returns></returns>
        public static Expr<T> Cond<T>(Expr<bool> cond, Expr<T> valueIfT, Expr<T> valueIfF)
        {
            return JSExpr<T>("(# ? # : #)", cond, valueIfT, valueIfF);
        }

        /// <summary>
        /// Ref(x) is the JavaScript reference to the observable variable x (normally observable variables
        /// are automatically dereferenced, giving their value instead).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="x"></param>
        /// <returns></returns>
        public static ObsVarRef<T> Ref<T>(ObsVar<T> x)
        {
            return new ObsVarRef<T> { Ref = x };
        }

        /// <summary>
        /// Ref(x) is the JavaScript reference to the observable variable x (normally observable variables
        /// are automatically dereferenced, giving their value instead).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="x"></param>
        /// <returns></returns>
        public static ComputedVarRef<T> Ref<T>(ComputedVar<T> x)
        {
            return new ComputedVarRef<T> { Ref = x };
        }

        /// <summary>
        /// NewObsVar(x) creates a new observable variable at runtime, initialised to value x.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static Expr<ObsVarRef<T>> NewObsVar<T>(Expr<T> x = null)
        {
            var t = typeof(T);

            var ctor =
                ( (t == typeof(int) || t == typeof(short) || t == typeof(long))
                ? "spanner.obsInt"
                : (t == typeof(double) || t == typeof(float) || t == typeof(decimal))
                ? "spanner.obsDouble"
                : (t.IsArray)
                ? "ko.observableArray"
                : "ko.observable"
                );

            return ((object)x == null)
                 ? JSExpr<ObsVarRef<T>>(ctor + "()")
                 : JSExpr<ObsVarRef<T>>(ctor + "(#)", x);
        }

        /// <summary>
        /// NewComputedVar(x) creates a new computed variable at runtime, with value x.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public static Expr<ComputedVarRef<T>> NewComputedVar<T>(Expr<T> x)
        {
            return JSExpr<ComputedVarRef<T>>("ko.computed(function () #)", Do(JSAct("return #", x)));
        }

        /// <summary>
        /// Deref(r) is the JavaScript for the value of the observable variable referred to by r.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="r"></param>
        /// <returns></returns>
        public static Expr<T> Deref<T>(Expr<ObsVarRef<T>> r)
        {
            return JSExpr<T>("#()", r);
        }

        /// <summary>
        /// Call(f) is equivalent to the JavaScript f().
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<T> Call<T>(Expr<Fn<T>> f)
        {
            return JSExpr<T>("#()", f);
        }

        /// <summary>
        /// Call(f, xa) is equivalent to the JavaScript f(xa).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<T> Call<Ta, T>(Expr<Fn<Ta, T>> f, Expr<Ta> xa)
        {
            return JSExpr<T>("#(#)", f, xa);
        }

        /// <summary>
        /// Call(f, xa, xb) is equivalent to the JavaScript f(xa, xb).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<T> Call<Ta, Tb, T>(Expr<Fn<Ta, Tb, T>> f, Expr<Ta> xa, Expr<Tb> xb)
        {
            return JSExpr<T>("#(#, #)", f, xa, xb);
        }

        /// <summary>
        /// Call(f, xa, xb, xc) is equivalent to the JavaScript f(xa, xb, xc).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<T> Call<Ta, Tb, Tc, T>(Expr<Fn<Ta, Tb, Tc, T>> f, Expr<Ta> xa, Expr<Tb> xb, Expr<Tc> xc)
        {
            return JSExpr<T>("#(#, #, #)", f, xa, xb, xc);
        }

        /// <summary>
        /// Call(f, xa, xb, xc, xd) is equivalent to the JavaScript f(xa, xb, xc, xd).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<T> Call<Ta, Tb, Tc, Td, T>(Expr<Fn<Ta, Tb, Tc, Td, T>> f, Expr<Ta> xa, Expr<Tb> xb, Expr<Tc> xc, Expr<Td> xd)
        {
            return JSExpr<T>("#(#, #, #, #)", f, xa, xb, xc, xd);
        }

        /// <summary>
        /// Fn(() => e) is equivalent to the JavaScript (function () { return e; }).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<Fn<T>> Fn<T>(Func<Expr<T>> f)
        {
            var body = f();

            var complex = body as ComplexExpr<T>;
            if ((object)complex == null)
            {
                Act ret = Do(JSAct((typeof(T) == typeof(Act) ? "#" : "return #"), body));
                return new FmtExpr<Fn<T>>(
                    "Fn",
                    new FmtStr("(function () "),
                    ret,
                    new FmtIndentation(),
                    new FmtStr(")")
                );
            }
            else
            {
                return new FmtExpr<Fn<T>>(
                    "Fn",
                    new FmtStr("(function () "),
                    Do(Do(complex.Acts), JSAct("return #", complex.Result)),
                    new FmtIndentation(),
                    new FmtStr(")")
                );
            }
        }

        // I am rather proud of this little cutie.  Look, no Expression lambdas!
        private static List<string> funcParamNames(Delegate f)
        {
            return f.Method.GetParameters().Select(x => x.Name).ToList();
        }

        /// <summary>
        /// Fn(xa => e) is equivalent to the JavaScript (function (xa) { return e; }).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<Fn<Ta, T>> Fn<Ta, T>(Func<Param<Ta>, Expr<T>> f)
        {
            var paramNames = funcParamNames(f);
            var xa = new Param<Ta> { BaseName = paramNames[0] };
            var ret = (typeof(T) == typeof(Act)) ? Do(f(xa)) : JSAct("return #", f(xa));
            return JSExpr<Fn<Ta, T>>("(function (#) #)", xa, Do(ret));
        }

        /// <summary>
        /// Fn(xa, xb => e) is equivalent to the JavaScript (function (xa, xb) { return e; }).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<Fn<Ta, Tb, T>> Fn<Ta, Tb, T>(Func<Param<Ta>, Param<Tb>, Expr<T>> f)
        {
            var paramNames = funcParamNames(f);
            var xa = new Param<Ta> { BaseName = paramNames[0] };
            var xb = new Param<Tb> { BaseName = paramNames[1] };
            var ret = (typeof(T) == typeof(Act)) ? Do(f(xa, xb)) : JSAct("return #", f(xa, xb));
            return JSExpr<Fn<Ta, Tb, T>>("(function (#, #) #)", xa, xb, Do(ret));
        }

        /// <summary>
        /// Fn(xa, xb, xc => e) is equivalent to the JavaScript (function (xa, xb, xc) { return e; }).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<Fn<Ta, Tb, Tc, T>> Fn<Ta, Tb, Tc, T>(Func<Param<Ta>, Param<Tb>, Param<Tc>, Expr<T>> f)
        {
            var paramNames = funcParamNames(f);
            var xa = new Param<Ta> { BaseName = paramNames[0] };
            var xb = new Param<Tb> { BaseName = paramNames[1] };
            var xc = new Param<Tc> { BaseName = paramNames[2] };
            var ret = (typeof(T) == typeof(Act)) ? Do(f(xa, xb, xc)) : JSAct("return #", f(xa, xb, xc));
            return JSExpr<Fn<Ta, Tb, Tc, T>>("(function (#, #, #) #)", xa, xb, xc, Do(ret));
        }

        /// <summary>
        /// Fn(xa, xb, xc, xd => e) is equivalent to the JavaScript (function (xa, xb, xc, xd) { return e; }).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<Fn<Ta, Tb, Tc, Td, T>> Fn<Ta, Tb, Tc, Td, T>(Func<Param<Ta>, Param<Tb>, Param<Tc>, Param<Td>, Expr<T>> f)
        {
            var paramNames = funcParamNames(f);
            var xa = new Param<Ta> { BaseName = paramNames[0] };
            var xb = new Param<Tb> { BaseName = paramNames[1] };
            var xc = new Param<Tc> { BaseName = paramNames[2] };
            var xd = new Param<Td> { BaseName = paramNames[3] };
            var ret = (typeof(T) == typeof(Act)) ? Do(f(xa, xb, xc, xd)) : JSAct("return #", f(xa, xb, xc, xd));
            return JSExpr<Fn<Ta, Tb, Tc, Td, T>>("(function (#, #, #, #) #)", xa, xb, xc, xd, Do(ret));
        }

        /// <summary>
        /// Proc(() => a) is equivalent to the JavaScript (function () { a; }).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<Fn<Act>> Proc(Act a)
        {
            return JSExpr<Fn<Act>>("(function () #)", Do(a));
        }

        /// <summary>
        /// Proc(xa => e) is equivalent to the JavaScript (function (xa) { a; }).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<Fn<Ta, Act>> Proc<Ta>(Func<Param<Ta>, Act> f)
        {
            var paramNames = funcParamNames(f);
            var xa = new Param<Ta> { BaseName = paramNames[0] };
            return JSExpr<Fn<Ta, Act>>("(function (#) #)", xa, Do(f(xa)));
        }

        /// <summary>
        /// Proc(xa, xb => e) is equivalent to the JavaScript (function (xa, xb) { a; }).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<Fn<Ta, Tb, Act>> Proc<Ta, Tb>(Func<Param<Ta>, Param<Tb>, Act> f)
        {
            var paramNames = funcParamNames(f);
            var xa = new Param<Ta> { BaseName = paramNames[0] };
            var xb = new Param<Tb> { BaseName = paramNames[1] };
            return JSExpr<Fn<Ta, Tb, Act>>("(function (#, #) #)", xa, xb, Do(f(xa, xb)));
        }

        /// <summary>
        /// Proc(xa, xb, xc => e) is equivalent to the JavaScript (function (xa, xb, xc) { a; }).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<Fn<Ta, Tb, Tc, Act>> Proc<Ta, Tb, Tc>(Func<Param<Ta>, Param<Tb>, Param<Tc>, Act> f)
        {
            var paramNames = funcParamNames(f);
            var xa = new Param<Ta> { BaseName = paramNames[0] };
            var xb = new Param<Tb> { BaseName = paramNames[1] };
            var xc = new Param<Tc> { BaseName = paramNames[2] };
            return JSExpr<Fn<Ta, Tb, Tc, Act>>("(function (#, #, #) #)", xa, xb, xc, Do(f(xa, xb, xc)));
        }

        /// <summary>
        /// Proc(xa, xb, xc, xd => e) is equivalent to the JavaScript (function (xa, xb, xc, xd) { a; }).
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<Fn<Ta, Tb, Tc, Td, Act>> Proc<Ta, Tb, Tc, Td>(Func<Param<Ta>, Param<Tb>, Param<Tc>, Param<Td>, Act> f)
        {
            var paramNames = funcParamNames(f);
            var xa = new Param<Ta> { BaseName = paramNames[0] };
            var xb = new Param<Tb> { BaseName = paramNames[1] };
            var xc = new Param<Tc> { BaseName = paramNames[2] };
            var xd = new Param<Td> { BaseName = paramNames[3] };
            return JSExpr<Fn<Ta, Tb, Tc, Td, Act>>("(function (#, #, #, #) #)", xa, xb, xc, xd, Do(f(xa, xb, xc, xd)));
        }

        /// <summary>
        /// ActExpr(a, e) is equivalent to the JavaScript (function () { a; return e; }).
        /// 
        /// Unclean!  Unclean!  Note that if the action executes a 'return' or throws an exception,
        /// behaviour is undefined.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="a"></param>
        /// <param name="x"></param>
        /// <returns></returns>
        public static Expr<T> ActExpr<T>(Act a, Expr<T> x)
        {
            return MakeComplexExpr(a, x);
        }

        public static Expr<bool> True = JSExpr<bool>("true");

        public static Expr<bool> False = JSExpr<bool>("false");

        /// <summary>
        /// Let(ea, xa => e) is equivalent to the JavaScript (function (xa) { return e; })(ea), although typically
        /// is optimised into just introducing an ordinary local variable rather than a temporary lambda.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="U"></typeparam>
        /// <param name="e"></param>
        /// <param name="f"></param>
        /// <returns></returns>
        public static Expr<U> Let<T, U>(Expr<T> e, Func<Param<T>, Expr<U>> f)
        {
            var paramNames = funcParamNames(f);
            var x = new Param<T> { BaseName = paramNames[0] };

            var act = new JSAct("var # = #", x, e);

            return MakeComplexExpr(act, f(x));
        }

        // Create a new complex expression, composing it with its result if that, too, is a complex expression.
        //
        internal static ComplexExpr<T> MakeComplexExpr<T>(Act act, Expr<T> result)
        {
            var complexResult = result as ComplexExpr<T>;

            var acts = ( (object)complexResult == null
                       ? (new [] { act })
                       : (new [] { act }).Concat(complexResult.Acts).ToArray()
                       );

            result =  ( (object)complexResult == null
                      ? result
                      : complexResult.Result
                      );

            return new ComplexExpr<T> { Acts = acts, Result = result };
        }

        // This class is needed to simplify nested let-expressions and the like.
        //
        internal class ComplexExpr<T> : Expr<T>, ITerm
        {
            internal Act[] Acts;
            internal Expr<T> Result;

            IEnumerable<IVar> ITerm.Dependencies()
            {
                return ((ITerm)Result).Dependencies().Concat(Acts.SelectMany(x => ((ITerm)x).Dependencies()));
            }

            void ITerm.Write(Context ctxt, JSorHtml what)
            {
                ctxt.WritePlain("(function() {");
                if (what == JSorHtml.JS) ctxt.WriteLine();
                ctxt.Indentation++;
                if (what == JSorHtml.JS) ctxt.WriteIndentation();
                ctxt.WriteTerm(Do(Acts), what);
                //foreach (var act in Acts) ctxt.WriteTerm((ITerm)act, what);
                if (what == JSorHtml.JS) ctxt.WriteLine();
                if (what == JSorHtml.JS) ctxt.WriteIndentation();
                ctxt.WritePlain("return ");
                ctxt.WriteTerm((ITerm)Result, what);
                ctxt.WritePlain(";");
                if (what == JSorHtml.JS) ctxt.WriteLine();
                ctxt.Indentation--;
                if (what == JSorHtml.JS) ctxt.WriteIndentation();
                ctxt.WritePlain("})()");
            }
        }

    }
}
